Go1.20 将禁止匿名接口循环导入!这是一次打破 Go1 兼容性承诺的真实案例
大家好,我是煎鱼。
最近因为临近新版本发布节点,我在看 Go1.20 的新特性《spec: disallow anonymous interface cycles[1]》,发现了一个比较骚的操作...以前我都没想到可以这么用,还有点意思,分享给大家。
在 Go 规范中是允许将接口类型(interface{})内嵌到其他声明的接口当中的,也就是著名的套娃神器:组合。
套娃接口类型
Go 标准库中比较经典的例子如下:
type ReadCloser interface {
Reader
Closer
}
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
实际上展开是:
type ReadCloser interface {
interface {
Read(p []byte) (n int, err error)
}
interface {
Close() error
}
}
一切都看起来如此美好,似乎很好的体现了 Go 的优秀之处。
计划是赶不上变化的。
匿名接口循环导入
在现实代码中,这种支持就存在着循环引用的用法。如下简单例子:
type I interface {
m()
interface {
I
}
}
这段代码,声明了接口类型 I,然后又包含了 m(),又包含接口 I。这会是一个 “永动机”,永远都不会停止。在开源的 GitHub 中,也真实存在着。
如项目 gozelus/zelus_rest[2] 的代码:
type MySQLDb interface {
execSQL
Table(ctx context.Context, name string) interface {
whereSQL
insertSQL
selectSQL
findSQL
orderSQL
clausesSQL
}
Begin() interface {
MySQLDb
Rollback()
Commit()
}
}
如项目 vetcher/go-astra[3] 的代码:
type ComplexInterface interface {
A(a interface {
B()
ComplexInterface
}) interface {
C()
D()
}
}
这类写法其实非常迷惑人,这意味可以无限嵌套接口,并使用内在的方法。但作者在写这个代码时,可能目的并不是如此,导致被使用者错用。
这有没有问题
对外宣传简洁好用瀑布式编程的 Go,如此对匿名接口循环导入的支持,是否合规呢?
其实并不然。
早在 2016 年的 Proposal: Type Aliases[4] 中的 Type cycles 部分就对此有所定义:
在类型别名的提案中明确指出:别名必须能够 "向外展开",没有办法展开出像 T = *T
这样的类型别名。
套用到现在的问题来,如果上面的 T 就是 I(接口类型),那么同理可得 I = *I
,这个过程是永远无法终止的。
社区讨论
在一番激烈讨论后,基于以下几点,决定接纳该提案,也就是在新版本中禁用 Go 匿名接口的循环导入,将其改为有限地扩展所有的嵌入式接口。
在禁用后,以下三种类似写法都会被拒绝。
第一种:
type B interface { I }
type I interface { m() interface { B } }
第二种:
type B = interface{ I }
type I interface{ m() interface{ B } }
第三种:
type B = interface{ I }
type I interface{ m() B }
Go1 兼容性承诺
最核心的是 Go1 兼容性承诺。从任何角度上来讲,禁用这个特性是破坏性变更(无法向后兼容),绝对是违反兼容性承诺的。
大家认为在公共项目库中,基本没有人使用这种匿名接口循环导入的方式,用途很少(几乎为 0)除了上面提到的 gozelus/zelus_rest 项目,并且该模块似乎没什么人引用。
rsc 在综合了利弊后,认为把这个特性干掉,能更好的提高代码简洁性,确立了该特性的禁用,会和以往一样的推进节奏。
如下:
Go1.20:Go 编译器默认会拒绝这些接口循环,但可以使用 go build -gcflags=all=-d=interfacecycles
来进行构建,以确保旧代码的正常编译。如果在候选发布期间有人向 Go 团队报告大量损坏,将会取消此更改。Go1.22:等到 1.22 版本后 -d=interfacecycles
标志将被删除,旧代码将不再构建该特性。如果有人报告问题,将可以讨论或是推迟删除,给予更多的改造时间。
链式调用模式
有一种经典的设计模式叫:链式调用,也有叫方法链的。例如在 etcd sdk 中,常常会在 Watch、Next 这类相关接口中见到。
在 Go 中可以这么写:
type Nexter interface {
Next(Input) (interface { Nexter }, error)
Done() Output
}
一旦禁用后,就不能如此匿名嵌套了。
会强烈推荐使用如下方式:
type Nexter interface {
Next(Input) (Nexter, error)
Done() Output
}
包括在 Node 这类节点声明时,也推荐如此:
type Node interface {
Parent() Node
FirstChild() Node
Children() []Node
}
套娃也得套上名字,不能成为 “无名” 者。
总结
原先支持匿名接口的循环导入,本质上违背了 Go 一贯的简洁明了的设计理念。如果在 Go 工程中用的多,不注意就会产生次生影响,禁了也有好处。
目前该特性变更的代码已经提交。如果按照 rsc 的计划我们会在 Go1.20 或 Go1.21 看到这个新特性,Go1.22 或 Go1.24 将会正式移除。
值得关注的一点,Go团队为此打破了对 Go1 兼容性的承诺,做出了破坏性变更,在推进方式上采取的是渐进式的模式。
这仍然值得我们关注,毕竟...破窗效应?
推荐阅读
参考资料
spec: disallow anonymous interface cycles: https://github.com/golang/go/issues/56103
[2]gozelus/zelus_rest: https://github.com/gozelus/zelus_rest/blob/master/core/db/db.go#L20
[3]vetcher/go-astra: https://github.com/vetcher/go-astra/blob/master/test/assets/interfaces/source.go#L52
[4]Proposal: Type Aliases: https://go.googlesource.com/proposal/+/master/design/18130-type-alias.md#type-cycles
关注和加煎鱼微信,
一手消息和知识,拉你进技术交流群👇
你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路。
日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!